﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Web.UI.WebControls.WebParts;
using Microsoft.SharePoint;
using Microsoft.SharePoint.WebPartPages;
using WebPart = System.Web.UI.WebControls.WebParts.WebPart;

namespace SPWebPartReplace
{
    /// <summary>
    /// Replaces all instances of a specified WebPart with a new WebPart
    /// instance on the designated site and subsites.
    /// </summary>
    public class WebPartReplace : IDisposable
    {
        private string _existingFullName;
        private Type _newWebPartType;
        private readonly SPSite _site;
        private readonly List<SPLimitedWebPartManager> _limitedWebPartManagers = new List<SPLimitedWebPartManager>();

        /// <summary>
        /// Initializes a new instance of the <see cref="WebPartReplace"/> class.
        /// </summary>
        /// <param name="site"></param>
        public WebPartReplace(SPSite site)
        {
            _site = site;
        }

        /// <summary>
        /// Executes the WebPart replacement.
        /// </summary>
        /// <param name="existingFullName">Fully qualified name of the existing WebPart.</param>
        /// <param name="newWebPart">An instance of the new WebPart.</param>
        public void ExecuteReplace(string existingFullName, WebPart newWebPart)
        {
            Console.WriteLine("WebPartReplace: _site.Url={0} existingFullName={1} newWebPart.GetType()={2}", _site.Url, existingFullName, newWebPart.GetType());
            _existingFullName = existingFullName;
            _newWebPartType = newWebPart.GetType();
            TraverseWebs(_site.AllWebs);
        }

        private void TraverseWebs(SPWebCollection webs)
        {
            for (int i = 0; i < webs.Count; i++)
            {
                SPWeb web = null;
                try
                {
                    web = webs[i];
                    web.AllowUnsafeUpdates = true;
                    if (web.Webs.Count > 0)
                    {
                        Console.WriteLine("Searching site: {0} containing {1} subsite(s)...",
                                                        web.ServerRelativeUrl, web.Webs.Count);
                        TraverseWebs(web.Webs);
                    }
                    Console.WriteLine("Searching site: {0} containing {1} folder(s)...",
                                                    web.ServerRelativeUrl, web.Folders.Count);
                    TraverseFolders(web.Folders);
                    Console.WriteLine("Searching site: {0} containing {1} files(s)...",
                                                    web.ServerRelativeUrl, web.Files.Count);
                    SearchFiles(web.Files);
                    web.AllowUnsafeUpdates = false;
                    web.Close();
                    web.Dispose();
                }
                catch(Exception ex)
                {
                    Console.WriteLine(
                        "An error occurred traversing the SPWebCollection: {0}{1}webs.Count={2}; index={3}{1}", ex,
                        Environment.NewLine, webs.Count, i);
                }
                finally
                {
                    foreach (SPLimitedWebPartManager limitedWebPartManager in _limitedWebPartManagers)
                    {
                        if (limitedWebPartManager != null)
                        {
                            if (limitedWebPartManager.Web != null)
                                limitedWebPartManager.Web.Dispose();
                            limitedWebPartManager.Dispose();
                        }
                    }

                    if (web != null)
                        web.Dispose();
                }
            }
        }

        private void TraverseFolders(SPFolderCollection folders)
        {
            if (folders.Folder.ContainingDocumentLibrary.Equals(Guid.Empty))
            {
                foreach (SPFolder folder in folders)
                {
                    if (folder.SubFolders.Count > 0)
                        TraverseFolders(folder.SubFolders);
                    if (folder.Files.Count > 0)
                    {
                        Console.WriteLine("Searching folder: {0} containing {1} files(s)...",
                                                        folder.ServerRelativeUrl, folder.Files.Count);
                        SearchFiles(folder.Files);
                    }
                }
            }
        }

        private void SearchFiles(SPFileCollection files)
        {
            foreach (SPFile file in files)
            {
                SearchWebParts(file);
            }
        }

        private void SearchWebParts(SPFile file)
        {
            try
            {
                FileInfo fileInfo = new FileInfo(file.Name);
                if (fileInfo.Extension.Contains("aspx"))
                {
                    SPLimitedWebPartManager webPartManager = file.GetLimitedWebPartManager(PersonalizationScope.Shared);
                    if (webPartManager != null)
                    {
                        _limitedWebPartManagers.Add(webPartManager);
                        Console.WriteLine("Searching file: {0} containing {1} webpart(s)...", file.ServerRelativeUrl, webPartManager.WebParts.Count);
                        for (int i = 0; i < webPartManager.WebParts.Count; i++)
                        {
                            using (WebPart wp = webPartManager.WebParts[i])
                            {
                                ReplaceWebPart(wp, webPartManager, file);
                            }
                        }
                        RemoveDuplicateWebParts(webPartManager);
                    }
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine("An unexpected error has occured searching {0}{1}{2}", file.ServerRelativeUrl,
                                  Environment.NewLine, ex);
            }
        }

        private void RemoveDuplicateWebParts(SPLimitedWebPartManager webPartManager)
        {
            List<string> webPartApplications = new List<string>();
            for (int i = 0; i < webPartManager.WebParts.Count; i++)
            {
                using (WebPart wp = webPartManager.WebParts[i])
                {
                    string application = GetWebPartApplication(wp);

                    if (application != null)
                    {
                        if (webPartApplications.Contains(application))
                        {
                            webPartManager.DeleteWebPart(wp);
                        }
                        else
                        {
                            webPartApplications.Add(application);
                        }
                    }
                }
            }
        }

        private string GetWebPartApplication(WebPart wp)
        {
            if (wp.GetType() == _newWebPartType)
            {
                PropertyInfo[] properties = wp.GetType().GetProperties();
                foreach (PropertyInfo propertyInfo in properties)
                {
                    if (propertyInfo.Name == "ApplicationName")
                        return propertyInfo.GetValue(wp, null) as string;
                }
            }
            return null;
        }

        private void ReplaceWebPart(object obj, SPLimitedWebPartManager webPartManager, SPFile file)
        {
            Type webPartType = obj.GetType();
            Console.WriteLine("Found webpart: {0}", webPartType.FullName);
            if (webPartType.FullName == _existingFullName)
            { 
                using (WebPart existingWebPart = (WebPart) obj)
                {
                    existingWebPart.ExportMode = WebPartExportMode.All;
                    string existingWebPartId = null;
                    string newWebPartId = null;
                    try
                    {
                        WebPart newWebPart = (WebPart)Activator.CreateInstance(_newWebPartType);
                        existingWebPartId = existingWebPart.ID;
                        newWebPartId = newWebPart.ID;

                        string existingZoneId = webPartManager.GetZoneID(existingWebPart);
                        int existingZoneIndex = existingWebPart.ZoneIndex;

                        CopyProperties(existingWebPart, newWebPart);
                        webPartManager.DeleteWebPart(existingWebPart);
                        Console.WriteLine("{0} deleted from {1}", existingWebPart.GetType(), file.ServerRelativeUrl);

                        webPartManager.AddWebPart(newWebPart, existingZoneId, existingZoneIndex);
                        Console.WriteLine("{0} added to {1}", newWebPart.GetType(), file.ServerRelativeUrl);
                    }
                    catch (WebPartPageUserException ex)
                    {
                        string message = string.Empty;
                        if (ex.Message.Contains("A Web Part with this ID has already been added to this page."))
                        {
                            message = string.Format("Existing web part ID: {0}{1}New web part ID: {2}", existingWebPartId, Environment.NewLine, newWebPartId);
                        }
                        Console.WriteLine("{0}{1}{2}", message, Environment.NewLine, ex);
                        throw;
                    }
                    catch (Exception ex)
                    {
                        Console.WriteLine(ex.ToString());
                        throw;
                    }
                }
            }
        }

        /// <summary>
        /// Copies the properties from the existing webpart to the new webpart using .NET Reflection.
        /// </summary>
        /// <param name="existingWebPart"></param>
        /// <param name="newWebPart"></param>
        private void CopyProperties(WebPart existingWebPart, WebPart newWebPart)
        {
            PropertyInfo[] properties = existingWebPart.GetType().GetProperties();

            foreach (PropertyInfo property in properties)
            {
                if (property.PropertyType.IsPublic && property.CanWrite && property.CanWrite)
                {
                    var value = property.GetValue(existingWebPart, null);
                    PropertyInfo newProperty = newWebPart.GetType().GetProperty(property.Name, property.PropertyType);
                    if (newProperty != null)
                    {
                        newProperty.SetValue(newWebPart, value, null);
                    }
                }
            }
        }

        public void Dispose()
        {
            if (_site != null)
                _site.Dispose();
        }
    }
}